/**
 * Project: report2imolike
 * File name: transformer.cpp
 * Description:  This class is the main backend for processing data from IMO report
 *
 * @author Vladimir Burgić, Petnica Meteor Group http://meteori.rs, Copyright (C) 2011.
 * @version 0.2.1
 *
 * @see The GNU Public License 3 (GPL)
 */
/*
 *   This file is part of report2imolike project.
 *
 *   Report2imolike is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Report2imolike is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with report2imolike.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "transformer.h"
#include <QDir>
#include <QFile>
#include <QTextStream>
#include <QDate>
#include <QtDebug>


//Basic idea is to put all the code in two static functions, and use other two functions just to call them.

Transformer::Transformer(QObject * parent)
    : QObject(parent)
{
    this->outputDirName = "";
    this->solLongFileName = "";
    this->infix = "";
}

void Transformer::convert()
{
    if (inputFileNamesList.isEmpty()){
        emit parsingOutcome(UnaccessibleInputFile);
        return;
    }
    if (solLongFileName == ""){
        emit parsingOutcome(NoSolLongFile);
        return;
    }
    if (outputDirName == ""){
        emit parsingOutcome(UnaccessibleOutputDir);
        return;
    }

    foreach (QString inputFileName, inputFileNamesList){
        ERR_CODES output = convert2files(inputFileName, this->solLongFileName, this->outputDirName, this->infix);
        emit parsingOutcome(output);
        qDebug() << output;
    }
}

void Transformer::convert(QStringList & rateLines, QStringList & magLines)
{
    if (inputFileNamesList.isEmpty()){
        emit parsingOutcome(UnaccessibleInputFile);
        return;
    }
    if (solLongFileName == ""){
        emit parsingOutcome(NoSolLongFile);
        return;
    }

    foreach (QString inputFileName, inputFileNamesList){
        ERR_CODES output = convert2strings (inputFileName, this->solLongFileName, rateLines, magLines);
        emit parsingOutcome(output);
        qDebug() << output;
    }
}

Transformer::ERR_CODES Transformer::convert2files (QString inputFileName, QString solLongFileName, QString outputDirName, QString infix)
{
    QStringList rateLines, magLines;
    ERR_CODES isOK = convert2strings(inputFileName, solLongFileName, rateLines, magLines);
    if (isOK != OK)
        return isOK;
    QDir outputDir (outputDirName);

    if (!outputDir.isReadable())
        return UnaccessibleOutputDir;

    QStringList filesInDir = outputDir.entryList();

    // We need to find out a year in order to properly name output files. But we already have rate and mag lines, so, we can use them:
    QString year = rateLines.at(0).simplified().section(" ", 7, 7);
    // now we know the year
    QString ratesFileName = "RATE" + infix + year + ".txt";
    QString magFileName = "MAGN" + infix + year + ".txt";

    // rate file
    if (filesInDir.contains(ratesFileName)){
        QFile ratesFile(outputDir.filePath(ratesFileName));
        if (!ratesFile.open(QIODevice::Append|QIODevice::Text) || !ratesFile.isWritable())
            return UnaccessibleOutputDir;
        else{
            QTextStream stream (&ratesFile);
            for (int i = 0; i < rateLines.size(); ++i)
                stream << rateLines.at(i).toLocal8Bit().constData() << endl;
        }
        ratesFile.close();
    }
    else{
        QFile ratesFile(outputDir.filePath(ratesFileName));
        if (!ratesFile.open(QIODevice::WriteOnly|QIODevice::Text) || !ratesFile.isWritable())
            return UnaccessibleOutputDir;
        else{
            QTextStream stream (&ratesFile);
            stream << endl << rateHeader << endl << QString (135, '-') << endl;
            for (int i = 0; i < rateLines.size(); ++i)
                stream << rateLines.at(i).toLocal8Bit().constData() << endl;
        }
        ratesFile.close();
    }


    // magn file
    if (filesInDir.contains(magFileName)){
        QFile magFile(outputDir.filePath(magFileName));
        if (!magFile.open(QIODevice::Append|QIODevice::Text) || !magFile.isWritable())
            return UnaccessibleOutputDir;
        else{
            QTextStream stream (&magFile);
            for (int i = 0; i < magLines.size(); ++i)
                stream << magLines.at(i).toLocal8Bit().constData() << endl;
        }
        magFile.close();
    }
    else{
        QFile magFile(outputDir.filePath(magFileName));
        if (!magFile.open(QIODevice::WriteOnly|QIODevice::Text) || !magFile.isWritable())
            return UnaccessibleOutputDir;
        else{
            QTextStream stream (&magFile);
            stream << endl << magHeader << endl << QString (144, '-') << endl;
            for (int i = 0; i < magLines.size(); ++i)
                stream << magLines.at(i).toLocal8Bit().constData() << endl;
        }
        magFile.close();
    }


    return OK;
}

Transformer::ERR_CODES Transformer::convert2strings (QString inputFileName, QString solLongFileName, QStringList & rateLines, QStringList & magLines)
{
    QMultiHash<QString, QString> inputFileAsList;
    QStringList streamList, periodList, distributionList;

    QFile * inputFile = new QFile(inputFileName);
    if (!inputFile->open(QIODevice::ReadOnly | QIODevice::Text))
        return UnaccessibleInputFile;

    QFile * solLongFile = new QFile(solLongFileName);
    if (!solLongFile->open(QIODevice::ReadOnly | QIODevice::Text))
        return NoSolLongFile;

    QTextStream inputFileText(inputFile);
    QString inputFileLine = inputFileText.readLine();
    while (!inputFileLine.isNull()){
        if ((inputFileLine.left(2) != "//") && !(inputFileLine.isEmpty())){
            inputFileLine = inputFileLine.simplified();
            QString key = inputFileLine.section(" ",0,0); //what comes before first space is key
            QString value = inputFileLine.section(" ",1); //what goes after first space is value for that key

            if (key == "shower")
                streamList.append(value.section(" ", 0, 0)); //only here we need only shower name, nothing after that
            if (key == "period")
                periodList.append(value);
            if (key == "distribution")
                distributionList.append(value);

            if ((key != "shower") && (key != "period") && (key !="distribution"))
                inputFileAsList.insert(key, value);

            qDebug() << key << " --> " << value;
        }
        inputFileLine = inputFileText.readLine();
    }

    inputFile->close();
    solLongFile->close();

    bool parsable =  parse(inputFileAsList, streamList, periodList, distributionList, solLongFileName, rateLines, magLines);

    if (!parsable)
        return BadFileFormat;

    return OK;
}


void Transformer::setOutputDir (const QString & dirName)
{
    this->outputDirName = dirName;
    bool allSet = (this->outputDirName != "") && !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2file(allSet);

    // only for debuging purpose
    qDebug() << this->outputDirName;
}

void Transformer::setSolLongFile (const QString & sLongFileName)
{
    this->solLongFileName = sLongFileName;
    bool allSet = (this->outputDirName != "") && !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2file(allSet);

    bool setForStrings = !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2string(setForStrings);

    // only for debuging purpose
    qDebug() << this->solLongFileName;
}

void Transformer::setInputFiles (const QString & fileForParsing)
{
    this->inputFileNamesList.clear();
    this->inputFileNamesList.append(fileForParsing);
    bool allSet = (this->outputDirName != "") && !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2file(allSet);

    bool setForStrings = !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2string(setForStrings);

    // only for debuging purpose
    qDebug() << this->inputFileNamesList.join("\n");
}

void Transformer::setInputFiles (const QStringList & filesForParsing)
{
    this->inputFileNamesList = filesForParsing;
    bool allSet = (this->outputDirName != "") && !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2file(allSet);

    bool setForStrings = !(this->inputFileNamesList.isEmpty()) && (this->solLongFileName != "");
    emit parsable2string(setForStrings);

    // only for debuging purpose
    qDebug() << this->inputFileNamesList.join("\n");
}

void Transformer::setInfix (const QString & infix)
{
    this->infix = infix;
    qDebug() << this->infix;
}

bool Transformer::parse(QMultiHash<QString, QString> inputData, QStringList streamList, QStringList periodList, QStringList distributionList, QString solLongFileName, QStringList &rateLines, QStringList &magLines)
{
    if ((!inputData.contains("night") && !(inputData.contains("date"))) || !inputData.contains("observer") || !inputData.contains("location") || periodList.isEmpty()  || distributionList.isEmpty())
        return false;

    /*
     Common part for both files is
        Name lmd phi DD MM YY TTTT-TTTT Sollong
     then, for RATE, there is
        Field   Teff F    lm   SPO Shw n
     and for MAG
        Shw  lm    -6    -5    -4    -3    -2    -1     0    +1    +2    +3    +4    +5    +6    +7  tot
     */


    // NIGHT

    /*
     It turns out that some reports have "night YYYY-MM-DD/DD" but others have "day YYYY-MM-DD".
     Even though we drop /DD, we'll add /32 to "day" strings, jost for debugging purpose.
     */
    QString night;
    if (inputData.contains("night"))
        night = inputData.value("night");
    else
        night = inputData.value("date") + "/32";

    night = night.section("/", 0, 0); //to grab YYYY-MM-DD from YYYY-MM-DD/DD
    QString day, month, year;
    year = night.section("-", 0, 0);
    year = year.right(2); //to extract 09 from 2009
    month = night.section("-", 1, 1);
    day = night.section("-", 2, 2);
    //finaly, to put it all in the right order
    night = day + " " + month + " " + year; // to get DD MM YY, as given at IMO files
    // NIGHT DONE

    // OBSERVER
    QString observer = inputData.value("observer");
    QString observerImoCode = observer.section(" ", 2, 2);
    if (observerImoCode.isEmpty()){
        QString observerName = observer.section(" ", 0, 0);
        QString observerSurname = observer.section(" ", 1, 1);
        observerImoCode = observerSurname.mid(1, 3) + observerName.mid(1, 2); //evading first character - quotation mark
    }
    else{
        observerImoCode = observerImoCode.mid(1, 5); //once again to remove quotation mark. This can be done via QString.replace("\"", ""), it might be bether, but for consistence with previous I've done it this way
    }
    observerImoCode = observerImoCode.toUpper();  //in case that any part of observer's name was in lower letters
    // OBSERVER END

    // POSITION (lmd phi)
    QString location = inputData.value("location");
    location.remove(","); //first to convert "DD MM SS E/W, DD MM SS N/S" to "DD MM SS E/W DD MM SS N/S"
    double lmdDegree = location.section(" ", 0, 0).toDouble();
    double lmdMinute = location.section(" ", 1, 1).toDouble();
    double lmdSecond = location.section(" ", 2, 2).toDouble();
    lmdDegree += (lmdMinute/60 + lmdSecond/3600); // seconds are unnecesary, because only first decimal number is considered, but for precission we will take them into account
    QString lmd = QString::number(lmdDegree, 'f', 1) + " " + location.section(" ", 3, 3);
    // if lambda is less than 100 \deg, we should add one space in front of it:
    lmd = lmd.rightJustified(7, ' ');

    double phiDegree = location.section(" ", 4, 4).toDouble();
    double phiMinute = location.section(" ", 5, 5).toDouble();
    double phiSecond = location.section(" ", 6, 6).toDouble();
    phiDegree += (phiMinute/60 + phiSecond/3600);
    QString phi = QString::number(phiDegree, 'f', 1) + " " + location.section(" ", 7, 7);
    // if phi is less than 10 \deg, we should add another space in front of it:
    phi = phi.rightJustified(6, ' ');

    // FIRST, LET'S CREATE COMMON PART (Name lmd phi DD MM YY)
    QString commonPart = observerImoCode + " " + lmd + " " + phi + " " + night;
    // COMMON PART CREATED


    // Creating RATELINES

    // Count showers, we need they number to know how far to go through "period" lines
    int streamsNumber = streamList.size();

    QString outputLine, timeInterval, rightAscension, declination, timeEff, clouds, limitMag, solLong;
    for (int i = 0; i < periodList.size(); ++i){
        //each line has format period: TTTT-TTTT RA Dec Teff F Lm SHOW1_method SHOW1_count SHOW2_method SHOW2_count ... SPO_count
        timeInterval = periodList.at(i).section(" ", 0, 0);
        timeInterval.replace ("-", " "); // we need TTTT TTTT instead of TTTT-TTTT

        rightAscension = periodList.at(i).section(" ", 1, 1);
        //to remove all the leading zeros
        int RA = rightAscension.toInt();
        rightAscension = QString::number(RA);
        //to add aditional spaces if necessary:
        rightAscension = rightAscension.rightJustified(3, ' ');

        declination = periodList.at(i).section(" ", 2, 2);
        // if there is "+" sign in front, we should get rid of it. Total length of string should be 3
        declination.remove("+");
        declination = declination.rightJustified (3, ' ');

        timeEff = periodList.at(i).section(" ", 3, 3);
        //we only need 2 decimals od timeEff
        // this can be done by searching for decimal point,, then looking at number of decimals, and finaly giving or taking appropriate number of decimals.
        // I consider folowing approach easier:
        double timeEffAsNumber = timeEff.toDouble();
        timeEff = QString::number (timeEffAsNumber, 'f', 2);

        clouds = periodList.at(i).section(" ", 4, 4);
        limitMag = periodList.at(i).section(" ", 5, 5);

         //we pass start and endr of the interval
        solLong = getSolLong(solLongFileName, timeInterval.section(" ", 0, 0), timeInterval.section(" ", 1, 1), night);
        solLong = solLong.rightJustified(7, ' ');


        outputLine = commonPart + " " + timeInterval + " " + solLong + " " + rightAscension + " " + declination + " " + timeEff + " " + clouds + " " + limitMag;

        //here come sporadics, but in "period" lines, they are last
        QString sporadicsCount = periodList.at(i).section(" ", -1, -1);
        outputLine += sporadicsCount.rightJustified(4, ' ');
        for (int j = 0; j  < (streamsNumber - 1); ++j){
            QString streamName = streamList.at(j);
            qDebug() << "streamName --> " << streamName;
            QString streamCount = periodList.at(i).section(" ", 7+2*j, 7+2*j);
            qDebug() << "streamCount --> " << streamCount;
            outputLine = outputLine + " " + streamName + streamCount.rightJustified(4, ' ');
        }

        rateLines.append(outputLine);
    }

    // RATELINES END

    // Creating MAGLINES

    // The main problem with distribution is that periods at this part don's neceserily match periods for counts part, but we need counts lines to get LMG

    for (int i = 0; i < distributionList.size(); ++i){
        QString distributionLine = distributionList.at(i);
        int intervalStart = distributionLine.section(" ", 1, 1).left(4).toInt(); //first 4 chars of TTTT-TTTT
        int intervalEnd = distributionLine.section(" ", 1, 1).right(4).toInt(); //last 4 chars of TTTT-TTTT
        // if interval is less than 1000, we'll add 2400 to intervalEnd (THIS CONDITION NEEDS DOUBLECHECK):
        if (intervalStart < 1000)
            intervalStart+=2400;
        if (intervalEnd < 1000)
            intervalEnd+=2400;

        // now let's find corresponding intervals within periods list
        int totalOverlap = 0, overlapStart, overlapEnd, overlap;
        float sumLMG = 0, lmg;
        for (int j = 0; j < periodList.size(); ++j){
            int periodIntervalStart = periodList.at(j).section(" ", 0, 0).left(4).toInt();
            int periodIntervalEnd = periodList.at(j).section(" ", 0, 0).right(4).toInt();
            if (periodIntervalStart < 1000)
                periodIntervalStart += 2400;
            if (periodIntervalEnd < 1000)
                periodIntervalEnd += 2400;
            overlapStart = (intervalStart > periodIntervalStart) ? intervalStart : periodIntervalStart;
            overlapEnd = (intervalEnd < periodIntervalEnd) ? intervalEnd : periodIntervalEnd;
            overlapStart = hhmm2minutes(overlapStart);
            overlapEnd = hhmm2minutes(overlapEnd);

            //if periods don't overlap, overlapStart will be greater than overlapEnd
            overlap = (overlapStart < overlapEnd) ? (overlapEnd - overlapStart) : 0;
            lmg = periodList.at(j).section(" ", 5, 5).toFloat();
            sumLMG += lmg * overlap;
            totalOverlap += overlap;
        }
        lmg = sumLMG / totalOverlap;
        limitMag = QString::number(lmg, 'f', 2);
        // now we have LMG given in two decimal places

        //at this moment we have Obsrv ... TTTT  (sollong SWR) lmg (-6 -5 ... 6 7). We miss sollong, showerName, and then actual data
        timeInterval= distributionLine.section(" ", 1, 1);
        timeInterval.replace("-", " ");
        solLong = getSolLong(solLongFileName, distributionLine.section(" ", 1, 1).left(4), distributionLine.section(" ", 1, 1).right(4), night);
        solLong = solLong.rightJustified(7, ' ');
        QString showerName = distributionLine.section(" ", 0, 0);
        outputLine = commonPart + " " + timeInterval + " " + solLong + " " + showerName + " " + limitMag;

        /*
        Now, all we have to do is to construct the part with actual numbers.
        The difference between input and output files is that at input file there are dashes ("-") and total number is given with one decimal (7.0); but at output file there are "0.0" in place of "-", and total number is gven as integer (7).
        We have earlier contracted all whitespaces to one. Now, we should insert whitespaces so that each two decimal points are 6 places away.
        */
        QString actualNumbers = distributionLine.section(" ", 2, -1);
        // replace dashes with 0.0
        actualNumbers.replace("-", " 0.0");
        // cut ".0" from the end, but we'll check whether there realy is .0 at the end
        if (actualNumbers.lastIndexOf(".") > (actualNumbers.size() -3))
            actualNumbers = actualNumbers.left(actualNumbers.lastIndexOf("."));

        //If we have "1.5 6.5" string, that means there are 3 chars between two decimal points. We always need 5. Index of dot - index of space is 2, but should be 4, so, we'll insert 2 spaces in place of "index of space". But, if it's last space, and there is not det after it, the rule is slight different, we slould add enough spaces to make string long indexOfSpace+5
        int spacesToAdd, indexOfSpace, indexOfDot = actualNumbers.indexOf(".");
        do
        {

            indexOfSpace = actualNumbers.indexOf(" ", indexOfDot+1);
            indexOfDot = actualNumbers.indexOf(".", indexOfSpace+1);
            if (indexOfDot == -1){
                actualNumbers.insert(indexOfSpace, QString(indexOfSpace + 5 - actualNumbers.size(), ' '));
            }
            else{
                spacesToAdd = 4 - (indexOfDot - indexOfSpace);
                actualNumbers.insert(indexOfSpace, QString (spacesToAdd, ' '));

                //just to be sure we're not playing with same space and dot all around:
                indexOfDot += spacesToAdd;
            }
        }
        while(indexOfDot != -1);
        //all actualNumbers string should have length 85
        actualNumbers = actualNumbers.rightJustified(85, ' ');
        outputLine = outputLine + " " + actualNumbers;
        magLines << outputLine;
    }
    //MAGLINES END

    return true;
}

QString Transformer::getSolLong(QString solLongFileName, QString timeStart, QString timeEnd, QString dateAsString)
{
    // if everything goes fine, the function will return solar longitude, otherwise, it will return -1

    QFile * solLongFile = new QFile(solLongFileName);

    dateAsString.replace(" ", "/");

    int hourStart = timeStart.left(2).toInt();
    int minStart = timeStart.right(2).toInt();
    int hourEnd = timeEnd.left(2).toInt();
    int minEnd = timeEnd.right(2).toInt();

    //now we'll make it all minutes
    minStart += 60*hourStart;
    minEnd += 60*hourEnd;

    //if there was midnight within the interval, let's add 24 hours:
    if (minEnd < minStart)
        minEnd += 24*60;

    //now it's time to find middle of the interval
    int midTime = (minStart + minEnd) / 2;

    int interval1, interval2;

    int positionStart, positionEnd;
    if (midTime % 120 == 0){ //if it's exactly some even hour
        positionStart = midTime/120;
        positionEnd = positionStart;
        interval1 = 60;
        interval2 = 60;
    }
    else{
        positionStart = midTime / 120;
        positionEnd = midTime / 120 + 1;

        interval1 = midTime - positionStart * 120;
        interval2 = positionEnd * 120 - midTime;

        if ((interval1 + interval2) != 120)
            qDebug() << "getSolLong - something is bad calculated";
    }

    QDate dateAsQDate(dateAsString.section("/", 2, 2).toInt(), dateAsString.section("/", 1, 1).toInt(),dateAsString.section("/", 0, 0).toInt());
    if (timeStart < "1200")
        dateAsQDate = dateAsQDate.addDays(1);
    QDate endDate = dateAsQDate;
    if (positionStart > 11){
        dateAsQDate = dateAsQDate.addDays(1);
        positionStart -= 12;
    }
    if (positionEnd > 11){
        endDate = endDate.addDays(1);
        positionEnd -= 12;
    }

    dateAsString = dateAsQDate.toString("dd/MM/yy");
    QString endDateString = endDate.toString("dd/MM/yy");

    qDebug() << "dateAsString --> " << dateAsString;
    qDebug() << "endDateString --> " << endDateString;

    if (!solLongFile->open(QIODevice::ReadOnly|QIODevice::Text)){
        return "-2";
    }

    float solLongStart = 0.0;
    float solLongEnd = 0.0;
    while (!solLongFile->atEnd()){
        QString line = solLongFile->readLine();
        line = line.simplified();
        if (line.section(" ", 0, 0) == dateAsString)
            solLongStart = line.section(" ", positionStart+1, positionStart+1).toFloat();
        qDebug() << "solLongStart --> " << solLongStart;
        if (line.section(" ", 0, 0) == endDateString)
            solLongEnd = line.section(" ", positionEnd+1, positionEnd+1).toFloat();
        qDebug() << "solLongEnd --> " << solLongEnd;
    }

    if ((solLongStart == 0.0) || (solLongEnd == 0.0)){
        qDebug() << "postitionStart -->" << positionStart;
        qDebug() << "postitionEnd -->" << positionEnd;
        return "-3";
    }

    float solarLongitude = (solLongStart * interval2 + solLongEnd * interval1) / 120;
    qDebug() << "solar longitud is OK, and is " << solarLongitude;
    return (QString::number(solarLongitude, 'f', 3));
}

int Transformer::hhmm2minutes(int timeAsString)
{
    QString time = QString::number(timeAsString);
    int hours = time.left(2).toInt();
    int minutes = time.right(2).toInt();
    return (minutes + 60 * hours);
}

QString Transformer::rateHeader = "Obsvr lmd     phi    DD MM YY TTTT-TTTT Sollong Field   Teff F    lm   SPO Shw n";
QString Transformer::magHeader = "Obsvr lmd     phi    DD MM YY TTTT-TTTT Sollong Shw  lm    -6    -5    -4    -3    -2    -1     0    +1    +2    +3    +4    +5    +6    +7  tot";
